{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "4786748e-cc06-4154-815e-6620946c04ad",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-07-25T13:52:59.912631Z",
     "iopub.status.busy": "2025-07-25T13:52:59.912631Z",
     "iopub.status.idle": "2025-07-25T13:53:37.823028Z",
     "shell.execute_reply": "2025-07-25T13:53:37.821965Z",
     "shell.execute_reply.started": "2025-07-25T13:52:59.912631Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001B[1m Human Message \u001B[0m=================================\n",
      "\n",
      "请检索知识库，并实时联网检索，韩立的名字是谁起的？韩立的结局是什么？\n",
      "==================================\u001B[1m Ai Message \u001B[0m==================================\n",
      "Tool Calls:\n",
      "  vec_kg (call_00r9KttjnIyBIpXsIhgWZbiS)\n",
      " Call ID: call_00r9KttjnIyBIpXsIhgWZbiS\n",
      "  Args:\n",
      "    question: 韩立的名字是谁起的？\n",
      "  fetch_real_time_info (call_QzXlMFBxoxiMpd7cnxtHppsg)\n",
      " Call ID: call_QzXlMFBxoxiMpd7cnxtHppsg\n",
      "  Args:\n",
      "    query: 韩立的结局\n",
      "  vec_kg (call_gi3ghV9O2TqPg3KFS9KSZm4d)\n",
      " Call ID: call_gi3ghV9O2TqPg3KFS9KSZm4d\n",
      "  Args:\n",
      "    question: 韩立的结局是什么？\n",
      "=================================\u001B[1m Tool Message \u001B[0m=================================\n",
      "Name: vec_kg\n",
      "\n",
      "\"\\u97e9\\u7acb\\u7684\\u7ed3\\u5c40\\u662f\\u8d70\\u4e0a\\u4e86\\u4e00\\u6761\\u4e0e\\u51e1\\u4eba\\u4e0d\\u540c\\u7684\\u4fee\\u4ed9\\u4e4b\\u8def\\u3002\\u4ed6\\u6700\\u521d\\u51fa\\u8fdc\\u95e8\\u662f\\u4e3a\\u4e86\\u6323\\u5927\\u94b1\\uff0c\\u4f46\\u6700\\u7ec8\\u5374\\u5b9e\\u73b0\\u4e86\\u81ea\\u5df1\\u7684\\u4fee\\u70bc\\u68a6\\u60f3\\u3002\\u867d\\u7136\\u94b1\\u8d22\\u5bf9\\u4ed6\\u5931\\u53bb\\u4e86\\u610f\\u4e49\\uff0c\\u97e9\\u7acb\\u7684\\u7ecf\\u5386\\u8ba9\\u4ed6\\u8e0f\\u4e0a\\u4e86\\u4ed9\\u4e1a\\u5927\\u9053\\u3002\"\n",
      "==================================\u001B[1m Ai Message \u001B[0m==================================\n",
      "\n",
      "根据知识库的检索，韩立的名字是由村里人所起的，外号是“二愣子”。这个名字的来源与村里已经有一个叫“愣子”的孩子有关。尽管韩立并不喜欢这个名字，但他只能默默接受。\n",
      "\n",
      "关于韩立的结局，来自实时联网信息的描述表明，韩立的结局是走上了一条与其他修士不同的修仙之路。起初，他出远门是为了赚取大量财富，但最后却实现了自己的修炼梦想，最终达到了一种自我修炼的境地。在经历诸多磨难之后，韩立在世俗利益面前选择了自己的信仰与道路，这与他最初的追求有了较大变化。\n"
     ]
    }
   ],
   "source": [
    "import os\n",
    "from langchain_openai import ChatOpenAI\n",
    "from typing import (Annotated,Sequence,TypedDict)\n",
    "from langchain_core.messages import BaseMessage\n",
    "from langgraph.graph.message import add_messages\n",
    "from langchain_core.tools import tool\n",
    "from typing import Union, Optional\n",
    "from pydantic import BaseModel, Field\n",
    "import json\n",
    "import http.client\n",
    "from langchain.prompts import PromptTemplate\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "os.environ[\"USER_AGENT\"] = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\"\n",
    "api_key=\"hk-xxx\"\n",
    "base_url=\"https://api.openai-hk.com/v1\"\n",
    "\n",
    "# 临时设置环境变量\n",
    "os.environ[\"OPENAI_API_KEY\"] = api_key\n",
    "os.environ[\"OPENAI_BASE_URL\"] = base_url\n",
    "# 实例化模型\n",
    "llm = ChatOpenAI(\n",
    "    api_key=api_key,\n",
    "    base_url=base_url,\n",
    "    model=\"gpt-4o-mini\"\n",
    ")\n",
    "# print(llm.invoke(\"介绍一下你自己\").content)\n",
    "\n",
    "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
    "from langchain_community.document_loaders import WebBaseLoader\n",
    "from langchain_milvus import Milvus\n",
    "from PyPDF2 import PdfReader\n",
    "from langchain_core.documents import Document\n",
    "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
    "from langchain_openai import OpenAIEmbeddings\n",
    "def pdf_read(pdf_doc):\n",
    "    text = \"\"\n",
    "    for pdf in pdf_doc:\n",
    "        pdf_reader = PdfReader(pdf)\n",
    "        for page in pdf_reader.pages:\n",
    "            text += page.extract_text()\n",
    "    return text\n",
    "\n",
    "content = pdf_read(['./data/凡人修仙传第一章.pdf'])\n",
    "\n",
    "chunk_size = 600\n",
    "chunk_overlap = 200\n",
    "\n",
    "text_splitter = RecursiveCharacterTextSplitter(\n",
    "    chunk_size=chunk_size, chunk_overlap=chunk_overlap\n",
    ")\n",
    "\n",
    "documents = [Document(page_content=content)]\n",
    "\n",
    "# 切分\n",
    "splits = text_splitter.split_documents(documents)\n",
    "embeddings = OpenAIEmbeddings(\n",
    "    model=\"text-embedding-3-small\",\n",
    ")\n",
    "\n",
    "# 添加到向量数据中\n",
    "vectorstore = Milvus.from_documents(\n",
    "    documents=splits,\n",
    "    collection_name=\"rag_milvus\",\n",
    "    embedding=embeddings,\n",
    "    connection_args={\n",
    "        \"uri\": \"https://xxx.serverless.gcp-us-west1.cloud.zilliz.com\",\n",
    "        \"user\": \"xxx\",\n",
    "        \"password\": \"xxx\",\n",
    "    }\n",
    ")\n",
    "\n",
    "\n",
    "class AgentState(TypedDict):\n",
    "    \"\"\"代理状态\"\"\"\n",
    "    messages: Annotated[Sequence[BaseMessage], add_messages]\n",
    "    \n",
    "class SearchQuery(BaseModel):\n",
    "    query: str = Field(description=\"Questions for networking queries\")\n",
    "\n",
    "\n",
    "@tool(args_schema = SearchQuery)\n",
    "def fetch_real_time_info(query):\n",
    "    \"\"\"获取互联网实时信息\"\"\"\n",
    "    conn = http.client.HTTPSConnection(\"google.serper.dev\")\n",
    "    payload = json.dumps({\n",
    "      \"q\": query,\n",
    "      \"gl\": \"cn\",\n",
    "      \"hl\": \"ny\"\n",
    "    })\n",
    "    headers = {\n",
    "      'X-API-KEY': 'xxx',\n",
    "      'Content-Type': 'application/json'\n",
    "    }\n",
    "    conn.request(\"POST\", \"/search\", payload, headers)\n",
    "    res = conn.getresponse()\n",
    "    data = res.read()\n",
    "    #print(data.decode(\"utf-8\"))\n",
    "    data=json.loads(data.decode(\"utf-8\"))# 将返回的JSON字符串转换为字典 \n",
    "    if 'organic' in data:\n",
    "        return json.dumps(data['organic'],  ensure_ascii=False)  # 返回'organic'部分的JSON字符串\n",
    "    else:\n",
    "        return json.dumps({\"error\": \"No organic results found\"},  ensure_ascii=False)  # 如果没有'organic'键，返回错误信息\n",
    "\n",
    "@tool\n",
    "def vec_kg(question:str):\n",
    "    \"\"\"\n",
    "    Personal knowledge base, which stores the interpretation of AI Agent project concepts\n",
    "    \"\"\"\n",
    "    prompt = PromptTemplate(\n",
    "        template=\"\"\"You are an assistant for question-answering tasks. \n",
    "        Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. \n",
    "        Use three sentences maximum and keep the answer concise:\n",
    "        Question: {question} \n",
    "        Context: {context} \n",
    "        Answer: \n",
    "        \"\"\",\n",
    "        input_variables=[\"question\", \"document\"],\n",
    "    )\n",
    "\n",
    "    '''\n",
    "    构建传统的RAG (Retrieval-Augmented Generation) Chain\n",
    "    使用LangChain的管道操作符(|)将prompt、语言模型(llm)和输出解析器(StrOutputParser)串联起来\n",
    "    流程: 用户输入 -> prompt模板处理 -> 语言模型生成 -> 字符串输出解析\n",
    "    '''\n",
    "    rag_chain = prompt | llm | StrOutputParser()\n",
    "\n",
    "    '''\n",
    "    构建检索器\n",
    "    将向量数据库(vectorstore)转换为检索器\n",
    "    search_kwargs={\"k\":1} 表示每次检索只返回最相关的1个文档片段 \n",
    "    '''\n",
    "    retriever = vectorstore.as_retriever(search_kwargs={\"k\": 1})\n",
    "\n",
    "    '''\n",
    "    执行检索 \n",
    "    用检索器获取与问题相关的文档片段\n",
    "    注意: 代码中直接使用了字符串\"question\"，实际应该使用变量question\n",
    "    '''\n",
    "    docs = retriever.invoke(question)\n",
    "    '''\n",
    "    生成最终答案\n",
    "    将检索到的文档(context)和原始问题(question)作为输入传递给RAG链 \n",
    "    RAG链会结合上下文信息生成答案 \n",
    "    '''\n",
    "    generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
    "    \n",
    "    return generation\n",
    "\n",
    "tools = [fetch_real_time_info, vec_kg]\n",
    "\n",
    "model = llm.bind_tools(tools)\n",
    "\n",
    "# ----------------------\n",
    "import json\n",
    "from langchain_core.messages import ToolMessage, SystemMessage, HumanMessage\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "\n",
    "tools_by_name = {tool.name: tool for tool in tools}\n",
    "\n",
    "\n",
    "# 定义工具节点\n",
    "def tool_node(state: AgentState):\n",
    "    outputs = []\n",
    "    for tool_call in state[\"messages\"][-1].tool_calls:\n",
    "        tool_result = tools_by_name[tool_call[\"name\"]].invoke(tool_call[\"args\"])\n",
    "        outputs.append(\n",
    "            ToolMessage(\n",
    "                content=json.dumps(tool_result),\n",
    "                name=tool_call[\"name\"],\n",
    "                tool_call_id=tool_call[\"id\"],\n",
    "            )\n",
    "        )\n",
    "    return {\"messages\": outputs}\n",
    "\n",
    "\n",
    "# 定义问答模型\n",
    "def call_model(\n",
    "    state: AgentState,\n",
    "):\n",
    "    system_prompt = SystemMessage(\n",
    "        \"你是一个智能助手，请尽全力回答用户的问题!\"\n",
    "    )\n",
    "    response = model.invoke([system_prompt] + state[\"messages\"])\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "\n",
    "# 定义路由节点\n",
    "def should_continue(state: AgentState):\n",
    "    messages = state[\"messages\"]\n",
    "    last_message = messages[-1]\n",
    "\n",
    "    if not last_message.tool_calls:\n",
    "        return \"end\"\n",
    "  \n",
    "    else:\n",
    "        return \"continue\"\n",
    "\n",
    "\n",
    "# ----------------------\n",
    "from langgraph.graph import StateGraph, END\n",
    "from IPython.display import Image, display\n",
    "# 定义一个图结构\n",
    "workflow = StateGraph(AgentState)\n",
    "\n",
    "# 在图结构中添加节点\n",
    "workflow.add_node(\"agent\", call_model)\n",
    "workflow.add_node(\"tools\", tool_node)\n",
    "\n",
    "# 设置启动点是 agent\n",
    "workflow.set_entry_point(\"agent\")\n",
    "\n",
    "# 添加路由边\n",
    "workflow.add_conditional_edges(\n",
    "    \"agent\",\n",
    "    should_continue,\n",
    "    {\n",
    "        \"continue\": \"tools\",\n",
    "        \"end\": END,\n",
    "    },\n",
    ")\n",
    "\n",
    "# 添加返回边\n",
    "workflow.add_edge(\"tools\", \"agent\")\n",
    "\n",
    "# 编译图\n",
    "graph = workflow.compile()\n",
    "# 展示绘图\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))\n",
    "\n",
    "# 打印问答的流\n",
    "def print_stream(stream):\n",
    "    for s in stream:\n",
    "        message = s[\"messages\"][-1]\n",
    "        if isinstance(message, tuple):\n",
    "            print(message)\n",
    "        else:\n",
    "            message.pretty_print()\n",
    "inputs = {\"messages\": [(\"user\", \"请检索知识库，并实时联网检索，韩立的名字是谁起的？韩立的结局是什么？\")]}\n",
    "print_stream(graph.stream(inputs, stream_mode=\"values\"))            "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fcd5f535-bd65-4c94-baa0-7fd4ec295bd0",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain",
   "language": "python",
   "name": "langchain"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
